Skip to content

Continuity plan: BC1–BC6 - #71

Merged
chaffed merged 10 commits into
mainfrom
bc1-gap
Oct 31, 2026
Merged

chaffed merged 10 commits into
mainfrom
bc1-gap

Conversation

@chaffed

@chaffed chaffed commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Summary

plans/CONTINUITY_PLAN.md BC1–BC6: what OpenClear does when something is down, and how a recovery is proven.

  • BC1 — an outage across a cut-off decides nothing by default. The sweep decided every open item whose deadline had passed, so the first sweep after a gap applied each customer's default to everything nobody could look at. Now a gap longer than the sweep's interval gives those items the bank's minimum review time again (never past a check's midnight deadline), records continuity.gap (high), emails the administrators and each customer's deciders, and the Controls page shows it for a week (OC-CP-02).
  • BC2 — GET /ready names the failing component (database, storage, Redis, the sweep's last run) and openclear_component_up{component=…} joins the metrics; /health stays the load balancer's check.
  • BC3 — backups measured. The host's backup job leaves backup_manifests/latest.json (shape in the runbook); Backups fresh (OC-CP-03) warns when it's missing or old. A SQLite installation copies itself nightly (VACUUM INTO) and writes the manifest.
  • BC4 — restores proven and reconciled. scripts/restore_drill.py restores a backup into a scratch database, migrates, verifies every action-log chain, reconciles rows against files and signs the result (restore_test); the newest passed drill is the last restore test on the Controls page. Nightly Files and rows agree (OC-CP-04); Platform → Continuity.
  • BC5 — maintenance mode (OPENCLEAR_MAINTENANCE_MODE=1: sign-in, reading and the data export work; other writes 503; the sweep pauses and the pause counts as a gap) and the bank's open items as a signed file, hourly with a dated daily copy, downloadable from Settings → Continuity and emailed to the incident contacts on request — what CUEC B-CP-2-01's plan works from.
  • BC6 — the runbook's "Recovering the installation".

Migrations: d4e8f2a6c1b7 (exception_item.deadline_moved_reason), e5f9a3b7d2c8 (restore_test), f7a1b5c9d3e2 (tenant.continuity_snapshot_emailed). New scheduled job continuity_snapshot_job (hourly). Tests now isolate every storage area under the test's temp directory.

Security checklist

  • New routes carry permission and CSRF checks (/ready is public like /health; the open-items download refuses customer-scoped sessions)
  • New tables and columns in the data dictionary; restore_test has no tenant_id (installation-wide)
  • Security events mapped (continuity.gap); notification types excluded from preferences where always-email
  • Bandit high clean; dependency audit clean
  • Suites on the branch head: see the final comment
  • No new dependency

🤖 Generated with Claude Code

Chaffed and others added 8 commits October 31, 2026 13:19
The disposition sweep decided every open item whose deadline had passed,
so the first sweep after OpenClear came back from being down across a
cut-off applied each customer's default to everything nobody could look
at (CONTINUITY_PLAN.md BC1). continuity_service.recover_from_gap runs at
the start of the sweep: when the sweep's own last run is older than its
interval plus five minutes, every open item whose deadline fell in the gap
gets the bank's minimum review time again, never past a check's midnight
deadline (deadline_moved_reason = "outage", migration d4e8f2a6c1b7); the
gap is in the bank's action log and a continuity.gap event (high); the
bank's administrators and each customer's deciders are emailed; the
Controls page shows it for a week (check continuity_gaps, control OC-CP-02).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
services/readiness.py checks the database, file storage (a probe byte
written and deleted), Redis when limits are shared through it, and
whether the cut-off sweep has run within its interval; /ready answers
ready, degraded (Redis or the sweep: the instance still serves) or
unavailable with a 503 (the database or storage), and the metrics gain
openclear_component_up{component=…}. /health stays the load balancer's
one-question check. The runbook says what to alert on (CONTINUITY_PLAN.md BC-D6).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The host's backup job leaves a manifest (backup_manifests/latest.json;
the runbook gives its shape) and the Controls page's "Backups fresh"
(control OC-CP-03) reads it, warning when there's none or the last backup
is older than OPENCLEAR_BACKUP_MAX_AGE_HOURS. A SQLite installation has no
backup job, so housekeeping makes a consistent copy nightly (VACUUM INTO),
keeps OPENCLEAR_SQLITE_BACKUP_KEEP_DAYS of them, and writes the manifest
itself (CONTINUITY_PLAN.md BC-D2, BC-D8). storage.read_key reads a file
OpenClear didn't write. The outage notice is excluded from the
notification preferences page, which enumerates every type (BC1's suite
found the gap).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/restore_drill.py restores a backup into a scratch database (a
copy of a SQLite file, or pg_restore), migrates it to head, verifies every
bank's action-log chain, reconciles the restored rows against file
storage, checks the dump's hash against the manifest, and records the
signed result as a restore_test row (migration e5f9a3b7d2c8); the newest
passed drill is the last restore test on every bank's Controls page, in
place of a date typed in. services/storage_reconcile.py walks every row
that names a file and every storage area (storage gains exists, ref and
list_keys on each backend): nightly as "Files and rows agree" (control
OC-CP-04, warning only) and from Platform → Continuity, which also shows
the backup manifest and the drills (CONTINUITY_PLAN.md BC-D2, BC-D3).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n plan

OPENCLEAR_MAINTENANCE_MODE=1: people sign in and look, an administrator
can take a data export, every other change answers 503 with the reason,
every page carries the notice, and the cut-off sweep pauses, marking when
it did so the pause counts as an outage across a cut-off when the mode
ends (continuity_service.note_paused / gap_before). The Controls page
shows the mode (check maintenance_mode).

services/continuity_snapshot.py writes each bank's open and pending items
hourly as a signed CSV in its storage, keeps a dated copy at the bank's
end of day, and emails it to the incident contacts when the bank asks
(tenant.continuity_snapshot_emailed, migration f7a1b5c9d3e2); Settings →
Continuity downloads them. It's what CUEC B-CP-2-01's plan works from when
OpenClear can't be reached (CONTINUITY_PLAN.md BC-D4, BC-D5).

Tests: the export, decision-file and audit-archive areas now live under
each test's temp directory like the others, so the nightly consistency
check no longer counts the repository's own data folders.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nine steps in the order each needs the one before: the banks told; the
keys from the key store before the database; maintenance mode on; the
restore and alembic upgrade head; file storage to the same point; /ready;
the action-log chains and the files-and-rows reconciliation; maintenance
mode off and what the first sweep then does with the gap; closing the
incident and recording the drill (CONTINUITY_PLAN.md BC-D7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…at go through the leader lock

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…found it; the SQLite drill test skips there

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chaffed
chaffed enabled auto-merge October 31, 2026 23:12
Comment thread src/openclear/storage.py Fixed
Comment thread src/openclear/storage.py Fixed
Chaffed and others added 2 commits October 31, 2026 16:17
…browser's string

CodeQL (#177–#179) traced `?day=` from the download route into a storage path. The route
parses the day as a date and builds the file name from that; anything else is a 404. The
local branch of `storage.read_key` now goes through `LocalStorage._path`, so its resolved
path is checked against the area like every other local read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chaffed
chaffed merged commit 8c0a236 into main Oct 31, 2026
23 checks passed
@chaffed
chaffed deleted the bc1-gap branch October 31, 2026 23:24
@chaffed chaffed mentioned this pull request Oct 31, 2026
2 tasks done
chaffed pushed a commit that referenced this pull request Oct 31, 2026
Version, migration head f7a1b5c9d3e2, the CHANGELOG section, the MASTER_PLAN note and the
security metrics for the continuity work (CONTINUITY_PLAN.md BC1–BC6, #71).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants